// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.workspace.storage.impl

import com.intellij.platform.workspace.storage.metadata.model.*
import java.io.File
import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.full.starProjectedType
import kotlin.reflect.jvm.jvmErasure

/**
 * File generates metadata constructors as strings to be used in codegen.
 *
 * Constructors are used during MetadataStorageImpl generation to initialize metadata of all types in the current package.
 * To do it, metadata classes constructors are inserted as a string in generated file.
 *
 * If the metamodel has been changed, add or remove classes from [CLASSES_TO_GENERATE] and run the [main] method.
 * Then Constructors.kt in [com.intellij.workspaceModel.codegen.impl.metadata.model] will be updated.
 */

internal fun main() {
  File(FILE_PATH).bufferedWriter().use { output ->
    output.write(codeBlock(FILE_HEADER))
    output.write(codeBlock(COLLECTION_CONSTRUCTOR_METHOD))
    output.write(codeBlock(METADATA_ENTITY_CONSTRUCTOR_METHOD))
    CLASSES_TO_GENERATE.forEach {
      output.write(codeBlock(generateConstructorMethod(it)))
    }
  }
}

// TODO("Find all classes with reflection")
private val CLASSES_TO_GENERATE: List<KClass<*>> = listOf(
  FinalClassMetadata.ClassMetadata::class,
  FinalClassMetadata.ObjectMetadata::class,
  FinalClassMetadata.EnumClassMetadata::class,
  FinalClassMetadata.KnownClass::class,
  ExtendableClassMetadata.AbstractClassMetadata::class,
  OwnPropertyMetadata::class,
  ExtPropertyMetadata::class,
  ValueTypeMetadata.ParameterizedType::class,
  ValueTypeMetadata.SimpleType.PrimitiveType::class,
  ValueTypeMetadata.SimpleType.CustomType::class,
  ValueTypeMetadata.EntityReference::class
)


private const val FILE_NAME = "Constructors.kt"

private const val PACKAGE_NAME = "com.intellij.workspaceModel.codegen.impl.metadata.model"

private val FILE_PATH = "community/platform/workspace/codegen-impl/src/main/kotlin/${PACKAGE_NAME.replace('.', '/')}/${FILE_NAME}"


private val FILE_HEADER = """
  package ${PACKAGE_NAME}
  
  import com.intellij.workspaceModel.codegen.impl.writer.*
  
  /**
   * This file is generated by com.intellij.platform.workspace.storage.metadata.utils.ConstructorsGenerator.kt.
   *
   * It is used during MetadataStorageImpl generation to initialize metadata of all types in the current package.
   * To do it, metadata classes constructors are inserted as a string in generated file.
   */
""".trimIndent()
private const val COLLECTION_CONSTRUCTOR_METHOD_NAME = "getCollectionConstructor"

private val COLLECTION_CONSTRUCTOR_METHOD = """
  internal fun $COLLECTION_CONSTRUCTOR_METHOD_NAME(any: Any?): String {
    if (any is Map.Entry<*, *>) {
      return "${'$'}{getCollectionConstructor(any.key)} to ${'$'}{getCollectionConstructor(any.value)}"
    }
    if (any is Iterable<*>) {
      val constructor = when (any) {
        is List<*> -> "listOf"
        is Set<*> -> "setOf"
        else -> "mapOf"
      }
      return "${'$'}constructor(${'$'}{any.joinToString(",\n") { getCollectionConstructor(it) }})"
    }
    return any.toString()
  }
""".trimIndent()

private val METADATA_ENTITY_CONSTRUCTOR_METHOD = """
  internal fun getEntityMetadataConstructor(fqName: String, entityDataFqName: String, supertypes: List<String>, properties: List<String>, extProperties: List<String>, isAbstract: Boolean): String {
    return "${'$'}EntityMetadata(fqName = ${'$'}fqName, entityDataFqName = ${'$'}entityDataFqName, supertypes = ${'$'}{getCollectionConstructor(supertypes)}, properties = ${'$'}{getCollectionConstructor(properties)}, extProperties = ${'$'}{getCollectionConstructor(extProperties)}, isAbstract = ${'$'}isAbstract)"
  }
""".trimIndent()


private fun generateConstructorMethod(metaClass: KClass<*>): String {
  val parameters = metaClass.constructorParameters
  return """
    internal fun get${metaClass.simpleName}Constructor(${parameters.joinToString(", ") { "${it.first}: ${it.second}" }}): String {
      return "$${metaClass.simpleName}(${parameters.joinToString(", ") { "${it.first} = ${generateParameterConstructorMethod(it)}" }})"
    }
  """.trimIndent()
}

private fun generateParameterConstructorMethod(parameter: Pair<String, String>): String {
  val (name, type) = parameter
  if (type.startsWith("List") || type.startsWith("Set") || type.startsWith("Map")) {
    return "\${$COLLECTION_CONSTRUCTOR_METHOD_NAME($name)}"
  }
  return "\$$name"
}



private fun codeBlock(string: String) = string + "\n\n"


private val KClass<*>.constructorParameters: List<Pair<String, String>>
  get() = constructorParametersKProperty.map { it.name to it.returnType.simpleNameWithGeneric }

private val KClass<*>.constructorParametersKProperty: List<KProperty1<*, *>>
  get() =
    primaryConstructor?.let { constructor ->
      declaredMemberProperties.filter { property ->
        constructor.parameters.any { parameter ->
          parameter.name == property.name && parameter.type == property.returnType
        }
      }
    } ?: emptyList()

private val KType.simpleNameWithGeneric: String
  get() {
    val generics: String = arguments.mapNotNull { it.type?.simpleNameWithGeneric }.joinToString(", ")
    return "${classifier?.simpleName}${if (generics.isNotEmpty()) "<$generics>" else ""}"
  }

private val KClassifier.simpleName: String?
  get() {
    val kClass = starProjectedType.jvmErasure
    return if (kClass.qualifiedName?.startsWith("kotlin") == true) kClass.simpleName else "String"
  }


